0%

SpringAI — 结构化输出

概述

大语言模型(LLM)生成结构化输出的能力,对于依赖可靠解析输出值的下游应用至关重要。开发者希望能够快速将 AI 模型的输出结果转换为 JSON、XML 或 Java 类等数据类型,以便传递给其他应用函数和方法使用。

Spring AI 的结构化输出转换器(Structured Output Converters) 帮助将 LLM 输出转换为结构化格式。如下图所示,这种方式围绕 LLM 文本补全端点(text completion endpoint)运作:

springai-structured-output

使用通用补全 API 从大语言模型(LLM)生成结构化输出,需要对输入和输出进行谨慎处理。结构化输出转换器在 LLM 调用前后都扮演着关键角色,确保最终获得期望的输出结构。

转换器的工作流程如下:

  • 在 LLM 调用之前,转换器会向提示词(Prompt)追加格式指令,为模型生成期望的输出结构提供明确指导。这些指令如同蓝图一般,引导模型的响应符合指定格式。
  • 在 LLM 调用之后,转换器接收模型的输出文本,并将其转换为结构化类型的实例。这一转换过程涉及解析原始文本输出,并将其映射到对应的结构化数据表示形式,例如 JSON、XML 或特定领域的数据结构。

StructuredOutputConverter 接口

StructuredOutputConverter 接口允许你获取结构化输出,例如将基于文本的 AI 模型输出映射为 Java 类或一组值。接口定义如下:

1
2
3
4
5
6
public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
}

public interface FormatProvider {
String getFormat();
}

下图展示了使用结构化输出 API 时的数据流:

springai-structured-output-dataflow

FormatProvider 向 AI 模型提供特定的格式指南,使其生成能够被 Converter 转换为指定目标类型 T 的文本输出。格式指令通常通过使用 PromptTemplate 追加到用户输入的末尾,示例如下:

  • FormatProvider:提供格式指令,告诉模型应该如何组织输出。
  • Converter:负责将模型的输出文本转换为指定类型 T 的实例。
1
2
3
4
5
6
7
8
String userInput = "Give me a list of actors and their filmographies";
String prompt = new PromptTemplate("""
{userInput}
{formatInstructions}
""").render(Map.of(
"userInput", userInput,
"formatInstructions", converter.getFormat()
));

内置转换器实现

Spring AI 目前提供了以下几种转换器实现:

转换器 说明
AbstractConversionServiceOutputConverter 提供预配置的 GenericConversionService,用于将 LLM 输出转换为期望的格式。不提供默认的 FormatProvider 实现。
AbstractMessageOutputConverter 提供预配置的 MessageConverter,用于将 LLM 输出转换为期望的格式。不提供默认的 FormatProvider 实现。
BeanOutputConverter 配置指定的 Java 类(如 Bean)或 ParameterizedTypeReference,使用 FormatProvider 实现指导 AI 模型生成符合 DRAFT_2020_12 JSON Schema 的 JSON 响应,然后利用 JsonMapper 将 JSON 输出反序列化为目标类的 Java 对象实例。
MapOutputConverter 扩展 AbstractMessageOutputConverter,包含引导 AI 模型生成符合 RFC8259 标准的 JSON 响应的 FormatProvider 实现,并利用 MessageConverter 将 JSON 负载转换为 java.util.Map 实例。
ListOutputConverter 扩展 AbstractConversionServiceOutputConverter,包含针对逗号分隔列表输出的 FormatProvider 实现,利用 ConversionService 将模型文本输出转换为 java.util.List

内置转换使用示例

BeanOutputConverter — 演员电影作品

以下示例展示如何使用 BeanOutputConverter 生成演员的电影作品列表。

目标记录类(Record):

1
public record ActorsFilms(String actor, List<String> films) {}

使用高级流式 ChatClient API:

1
2
3
4
5
6
7
8
ChatClient chatClient = ChatClient.builder(chatModel).build();

ActorsFilms result = chatClient.prompt()
.user("Generate the filmography for the actor Tom Hanks.")
.call()
.entity(ActorsFilms.class);

System.out.println(result);

或直接使用底层 ChatModel API:

1
2
3
4
5
6
7
8
9
10
11
BeanOutputConverter<ActorsFilms> converter = new BeanOutputConverter<>(ActorsFilms.class);

String prompt = "Generate the filmography for the actor Tom Hanks.\n"
+ converter.getFormat();

ChatResponse response = chatModel.call(
new Prompt(prompt, ChatOptions.builder().build())
);

ActorsFilms result = converter.convert(response.getResult().getOutput().getText());
System.out.println(result);

自定义属性排序

BeanOutputConverter 支持通过 @JsonPropertyOrder 注解在生成的 JSON Schema 中自定义属性排序。该注解允许你指定属性在 Schema 中出现的确切顺序,不受类中属性声明顺序的影响:

1
2
@JsonPropertyOrder({"actor", "films"})
public record ActorsFilms(String actor, List<String> films) {}

该注解同时适用于 Record 和常规 Java 类。

使用 ParameterizedTypeReference

使用 ParameterizedTypeReference 构造函数可以指定更复杂的目标类结构。例如,表示多个演员及其电影作品列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
ParameterizedTypeReference<List<ActorsFilms>> typeRef = 
new ParameterizedTypeReference<>() {};

BeanOutputConverter<List<ActorsFilms>> converter =
new BeanOutputConverter<>(typeRef);

// 使用 ChatClient
List<ActorsFilms> result = chatClient.prompt()
.user("Generate the filmographies for Tom Hanks and Tom Cruise.")
.call()
.entity(typeRef);

System.out.println(result);

或直接使用底层 ChatModel API:

1
2
3
4
5
6
7
8
BeanOutputConverter<List<ActorsFilms>> converter = 
new BeanOutputConverter<>(typeRef);

String prompt = "Generate the filmographies for Tom Hanks and Tom Cruise.\n"
+ converter.getFormat();

ChatResponse response = chatModel.call(new Prompt(prompt));
List<ActorsFilms> result = converter.convert(response.getResult().getOutput().getText());

MapOutputConverter — 转换为 Map

以下示例展示如何使用 MapOutputConverter 将模型输出转换为包含数字列表的 Map:

1
2
3
4
5
6
7
8
MapOutputConverter converter = new MapOutputConverter();

String prompt = "Generate a map with key 'numbers' and value a list of prime numbers from 1 to 30.\n"
+ converter.getFormat();

ChatResponse response = chatModel.call(new Prompt(prompt));
Map<String, Object> result = converter.convert(response.getResult().getOutput().getText());
System.out.println(result);

或直接使用底层 ChatModel API:

1
2
MapOutputConverter converter = new MapOutputConverter();
// ... 同上

ListOutputConverter — 转换为 List

以下示例展示如何使用 ListOutputConverter 将模型输出转换为冰淇淋口味列表:

1
2
3
4
5
6
7
ListOutputConverter converter = new ListOutputConverter(new DefaultConversionService());

String prompt = "List 5 popular ice cream flavors.\n" + converter.getFormat();

ChatResponse response = chatModel.call(new Prompt(prompt));
List<String> result = converter.convert(response.getResult().getOutput().getText());
System.out.println(result);

或直接使用底层 ChatModel API:

1
2
ListOutputConverter converter = new ListOutputConverter(new DefaultConversionService());
// ... 同上

原生结构化输出(Native Structured Output)

许多现代 AI 模型现已提供对结构化输出的原生支持,与基于提示词的格式控制相比,这种方式能提供更可靠的结果。Spring AI 通过原生结构化输出(Native Structured Output) 特性支持这一能力。

使用原生结构化输出时,由 BeanOutputConverter 生成的 JSON Schema 会直接发送给模型的结构化输出 API,无需在提示词中追加格式指令。这种方式具有以下优势:

优势 说明
更高的可靠性 模型保证输出符合 Schema 约束
更简洁的提示词 无需追加格式指令
更好的性能 模型可以在内部针对结构化输出进行优化

启用原生结构化输出

要启用原生结构化输出,使用 AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT 参数:

1
2
3
4
5
6
7
8
9
10
11
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new StructuredOutputAdvisor())
.build();

List<ActorsFilms> result = chatClient.prompt()
.user("Generate the filmographies for Tom Hanks and Tom Cruise.")
.advisors(advisor -> advisor.params(
Map.of(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT, true)
))
.call()
.entity(typeRef);

也可以在使用 ChatClient.BuilderdefaultAdvisors() 时全局启用:

1
2
3
4
5
6
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(new StructuredOutputAdvisor())
.defaultAdvisors(advisor -> advisor.params(
Map.of(AdvisorParams.ENABLE_NATIVE_STRUCTURED_OUTPUT, true)
))
.build();

支持原生结构化输出的模型

模型提供商 支持情况
OpenAI GPT-4o 及更高版本,支持 JSON Schema
Anthropic Claude 3.5 Sonnet 及更高版本
Google GenAI Gemini 1.5 Pro 及更高版本
Mistral AI Mistral Small 及更高版本,支持 JSON Schema

Ollama 模型的注意事项

并非所有 Ollama 模型都能可靠地遵守结构化输出 Schema 约束。特别是内置了推理或”思考”模式的模型(例如 qwen3:8bqwen3.5:9b 及其他较新的 Qwen 变体),可能会将内部推理过程以纯文本形式返回,而非结构化 JSON,从而导致 BeanOutputConverter 反序列化错误,例如:

1
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type ...

如果在 Ollama 中遇到此问题,可以尝试换用其他模型(例如 llama3.1:latest),或回退到默认的基于提示词的方式。你也可以结合使用 useProviderStructuredOutput()validateSchema(),使格式错误的响应能够自动重试。

OpenAI 的限制

OpenAI 结构化输出 API 不接受顶层 JSON 数组作为响应 Schema(详见 OpenAI 社区讨论)。在启用原生结构化输出时请求 List 将导致 API 错误。请改用以下替代方案之一:

  • 使用包装类(Wrapper Class)代替 List
  • 使用 ParameterizedTypeReference 配合自定义结构

各模型的结构化输出配置选项

一些 AI 模型提供了专用配置选项来生成结构化(通常为 JSON)输出:

OpenAI

OpenAI 的结构化输出(Structured Outputs) 可以确保模型生成严格符合你提供的 JSON Schema 的响应。你可以选择:

  • JSON_OBJECT:保证模型生成的消息是有效的 JSON
  • JSON_SCHEMA:配合提供的 Schema,保证模型生成匹配你提供的 Schema 的响应

配置选项:spring.ai.openai.chat.response-format

Ollama

Ollama 提供 spring.ai.ollama.chat.format 选项来指定响应返回的格式。目前唯一接受的值为 json

Mistral AI

Mistral AI 提供 spring.ai.mistralai.chat.response-format 选项来指定响应返回的格式。设置为 json 可启用 JSON 模式,保证模型生成的消息是有效的 JSON。此外,设置为 { "type": "json_schema" } 并配合提供的 Schema,可启用原生结构化输出支持,保证模型生成匹配你提供的 Schema 的响应。